// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::{borrow::Cow, collections::BTreeSet};

use linera_base::{
    crypto::CryptoHash,
    data_types::{BlockHeight, Epoch},
    hashed::Hashed,
    identifiers::ChainId,
};
use linera_chain::types::Timeout;

use super::ValueCache;

/// Test cache size for unit tests.
const TEST_CACHE_SIZE: usize = 10;

/// Tests attempt to retrieve non-existent value.
#[test]
fn test_retrieve_missing_value() {
    let cache = ValueCache::<CryptoHash, Hashed<Timeout>>::new(TEST_CACHE_SIZE);
    let hash = CryptoHash::test_hash("Missing value");

    assert!(cache.get(&hash).is_none());
    assert!(cache.keys::<Vec<_>>().is_empty());
}

/// Tests inserting a certificate value in the cache.
#[test]
fn test_insert_single_certificate_value() {
    let cache = ValueCache::<CryptoHash, Hashed<Timeout>>::new(TEST_CACHE_SIZE);
    let value = create_dummy_certificate_value(0);
    let hash = value.hash();

    assert!(cache.insert(Cow::Borrowed(&value)));
    assert!(cache.contains(&hash));
    assert_eq!(cache.get(&hash), Some(value));
    assert_eq!(cache.keys::<BTreeSet<_>>(), BTreeSet::from([hash]));
}

/// Tests inserting many certificate values in the cache, one-by-one.
#[test]
fn test_insert_many_certificate_values_individually() {
    let cache = ValueCache::<CryptoHash, Hashed<Timeout>>::new(TEST_CACHE_SIZE);
    let values = create_dummy_certificate_values(0..(TEST_CACHE_SIZE as u64)).collect::<Vec<_>>();

    for value in &values {
        assert!(cache.insert(Cow::Borrowed(value)));
    }

    for value in &values {
        assert!(cache.contains(&value.hash()));
        assert_eq!(cache.get(&value.hash()).as_ref(), Some(value));
    }

    assert_eq!(
        cache.keys::<BTreeSet<_>>(),
        BTreeSet::from_iter(values.iter().map(Hashed::hash))
    );
}

/// Tests inserting many values in the cache, all-at-once.
#[test]
fn test_insert_many_values_together() {
    let cache = ValueCache::<CryptoHash, Hashed<Timeout>>::new(TEST_CACHE_SIZE);
    let values = create_dummy_certificate_values(0..(TEST_CACHE_SIZE as u64)).collect::<Vec<_>>();

    cache.insert_all(values.iter().map(Cow::Borrowed));

    for value in &values {
        assert!(cache.contains(&value.hash()));
        assert_eq!(cache.get(&value.hash()).as_ref(), Some(value));
    }

    assert_eq!(
        cache.keys::<BTreeSet<_>>(),
        BTreeSet::from_iter(values.iter().map(|el| el.hash()))
    );
}

/// Tests re-inserting many values in the cache, all-at-once.
#[test]
fn test_reinsertion_of_values() {
    let cache = ValueCache::<CryptoHash, Hashed<Timeout>>::new(TEST_CACHE_SIZE);
    let values = create_dummy_certificate_values(0..(TEST_CACHE_SIZE as u64)).collect::<Vec<_>>();

    cache.insert_all(values.iter().map(Cow::Borrowed));

    for value in &values {
        assert!(!cache.insert(Cow::Borrowed(value)));
    }

    for value in &values {
        assert!(cache.contains(&value.hash()));
        assert_eq!(cache.get(&value.hash()).as_ref(), Some(value));
    }

    assert_eq!(
        cache.keys::<BTreeSet<_>>(),
        BTreeSet::from_iter(values.iter().map(Hashed::hash))
    );
}

/// Tests eviction of one entry.
#[test]
fn test_one_eviction() {
    let cache = ValueCache::<CryptoHash, Hashed<Timeout>>::new(TEST_CACHE_SIZE);
    let values = create_dummy_certificate_values(0..=(TEST_CACHE_SIZE as u64)).collect::<Vec<_>>();

    cache.insert_all(values.iter().map(Cow::Borrowed));

    assert!(!cache.contains(&values[0].hash()));
    assert!(cache.get(&values[0].hash()).is_none());

    for value in values.iter().skip(1) {
        assert!(cache.contains(&value.hash()));
        assert_eq!(cache.get(&value.hash()).as_ref(), Some(value));
    }

    assert_eq!(
        cache.keys::<BTreeSet<_>>(),
        BTreeSet::from_iter(values.iter().skip(1).map(Hashed::hash))
    );
}

/// Tests eviction of the second entry.
#[test]
fn test_eviction_of_second_entry() {
    let cache = ValueCache::<CryptoHash, Hashed<Timeout>>::new(TEST_CACHE_SIZE);
    let values = create_dummy_certificate_values(0..=(TEST_CACHE_SIZE as u64)).collect::<Vec<_>>();

    cache.insert_all(values.iter().take(TEST_CACHE_SIZE).map(Cow::Borrowed));
    cache.get(&values[0].hash());
    assert!(cache.insert(Cow::Borrowed(&values[TEST_CACHE_SIZE])));

    assert!(cache.contains(&values[0].hash()));
    assert_eq!(cache.get(&values[0].hash()).as_ref(), Some(&values[0]));

    assert!(!cache.contains(&values[1].hash()));
    assert!(cache.get(&values[1].hash()).is_none());

    for value in values.iter().skip(2) {
        assert!(cache.contains(&value.hash()));
        assert_eq!(cache.get(&value.hash()).as_ref(), Some(value));
    }

    assert_eq!(
        cache.keys::<BTreeSet<_>>(),
        BTreeSet::from_iter(
            values
                .iter()
                .skip(2)
                .map(Hashed::hash)
                .chain(Some(values[0].hash()))
        )
    );
}

/// Tests if reinsertion of the first entry promotes it so that it's not evicted so soon.
#[test]
fn test_promotion_of_reinsertion() {
    let cache = ValueCache::<CryptoHash, Hashed<Timeout>>::new(TEST_CACHE_SIZE);
    let values = create_dummy_certificate_values(0..=(TEST_CACHE_SIZE as u64)).collect::<Vec<_>>();

    cache.insert_all(values.iter().take(TEST_CACHE_SIZE).map(Cow::Borrowed));
    assert!(!cache.insert(Cow::Borrowed(&values[0])));
    assert!(cache.insert(Cow::Borrowed(&values[TEST_CACHE_SIZE])));

    assert!(cache.contains(&values[0].hash()));
    assert_eq!(cache.get(&values[0].hash()).as_ref(), Some(&values[0]));

    assert!(!cache.contains(&values[1].hash()));
    assert!(cache.get(&values[1].hash()).is_none());

    for value in values.iter().skip(2) {
        assert!(cache.contains(&value.hash()));
        assert_eq!(cache.get(&value.hash()).as_ref(), Some(value));
    }

    assert_eq!(
        cache.keys::<BTreeSet<_>>(),
        BTreeSet::from_iter(
            values
                .iter()
                .skip(2)
                .map(Hashed::hash)
                .chain(Some(values[0].hash()))
        )
    );
}

/// Creates multiple dummy [`Hashed<Timeout>`]s to use in the tests.
fn create_dummy_certificate_values<Heights>(
    heights: Heights,
) -> impl Iterator<Item = Hashed<Timeout>>
where
    Heights: IntoIterator,
    Heights::Item: Into<BlockHeight>,
{
    heights.into_iter().map(create_dummy_certificate_value)
}

/// Creates a new dummy [`Hashed<Timeout>`] to use in the tests.
fn create_dummy_certificate_value(height: impl Into<BlockHeight>) -> Hashed<Timeout> {
    Hashed::new(Timeout::new(
        ChainId(CryptoHash::test_hash("Fake chain ID")),
        height.into(),
        Epoch(0),
    ))
}
